namespace RL

open Raylib_CsLo
open Garnet.Composition
open RL.Components
open RL.Helpers
open RL.Random
open System.Collections.Generic

module Tiles =
    /// Enum that represents the type of tile.
    type TileType =
    |   Wall = 0
    |   Floor = 1

    type Tile = struct
        val tileType : TileType
        val sprite : Sprite
        new(t, s) = { sprite = s; tileType = t }
    end

    /// Returns whether the provided tile blocks line-of-sight or not.
    let Obscures tile =
        match tile with
            | TileType.Wall -> true
            | _ -> false

    /// Returns whether the provided tile blocks movement or not.
    let Blocks tile =
        match tile with
            | TileType.Wall -> true
            | _ -> false

    let WallTile = new Tile(TileType.Wall, { spriteID = 35; color = Raylib.WHITE })
    let FloorTile = new Tile(TileType.Floor, { spriteID = 46; color = Raylib.GRAY })

module Rooms =
    type RectangularRoom(x : int, y : int, w : int, h : int) =
        member val x1 = x with get
        member val y1 = y with get
        member val x2 = x + w with get
        member val y2 = y + h with get

        member r.Inner = Seq.allPairs (seq { r.x1 .. r.x2 - 1 }) (seq { r.y1 .. r.y2 - 1 })
        member r.PointSet = [for x, y in r.Inner do yield { x = x; y = y }]

        member r.Center = { x = (r.x1 + r.x2) / 2; y = (r.y1 + r.y2) / 2 }
        member r.Intersects(o : RectangularRoom) =
            r.x1 < o.x2 + 1 && r.x2 > o.x1 - 1 && r.y1 < o.y2 + 1 && r.y2 > o.y1 - 1

    /// A container union which holds room structures, discriminated for purposes of room generation.
    type Room  =
        | Rectangular of r : RectangularRoom

module Mapping =
    open Tiles

    type RLMap(width : int, height : int) =
        member val Width = width with get
        member val Height = height with get
        member val MapArray = [|for _ in 1 .. width * height -> WallTile|] with get
        member val VisibleArray = [|for _ in 1 .. width * height -> false|] with get
        member val ExploredArray = [|for _ in 1 .. width * height -> false|] with get

        /// Checks if a point is within the map bounds.
        member m.InBounds x y =
            (x >= 0 && x < m.Width) && (y >= 0 && y < m.Height)

        /// Gets the array index from an X,Y coordinate.
        member inline m.Index x y = (y * m.Width) + x
        /// Gets a Point2D from an index value.
        member inline m.ReverseIndex i = { x = i % m.Width; y = i / m.Width }

        /// Returns the tile at X,Y.
        member m.GetTile x y =
            m.MapArray[m.Index x y]
        /// Sets the tile at X,Y to the given tile.
        member internal m.SetTile x y tile =
            m.MapArray[m.Index x y] <- tile

        // Checks if a map position is blocked by a tile.
        member m.IsPositionBlocked x y =
            Blocks <| m.MapArray[m.Index x y].tileType

        /// Clears the visible tiles array.
        member m.ClearVisible =
            for i in 0..m.VisibleArray.Length - 1 do
                m.VisibleArray[i] <- false
        /// Sets a tile to visible.
        member m.SetVisible x y =
            m.VisibleArray[m.Index x y] <- true
        /// Repopulates the explored array based on whether the tile is visible or has already been explored.
        member m.RefreshExplored =
            for i in 0..m.ExploredArray.Length - 1 do
                m.ExploredArray[i] <- (m.VisibleArray[i] || m.ExploredArray[i])

        /// Gets a list of visible points.
        member m.GetPointsVisible =
            seq { 0 .. m.MapArray.Length - 1 }
            |> Seq.filter ( fun i -> m.VisibleArray[i] )
            |> Seq.map ( fun i -> m.ReverseIndex i )
            |> Seq.toList

        /// Gets a list of explored points (not including visible ones).
        member m.GetPointsExploredExclusive =
            seq { 0 .. m.MapArray.Length - 1 }
            |> Seq.filter ( fun i -> m.ExploredArray[i] && not m.VisibleArray[i] )
            |> Seq.map ( fun i -> m.ReverseIndex i )
            |> Seq.toList

        /// Gets a list of explored points.
        member m.GetPointsExplored =
            seq { 0 .. m.MapArray.Length - 1 }
            |> Seq.filter ( fun i -> m.ExploredArray[i] )
            |> Seq.map ( fun i -> m.ReverseIndex i )
            |> Seq.toList

        member m.GetAvailableExits(index : int, blocks : int list) =
            let validExit pos delta =
                let dest : Point2D = pos + delta
                if (m.InBounds dest.x dest.y) && not (m.IsPositionBlocked dest.x dest.y) then
                    Some(m.Index dest.x dest.y)
                else
                    None
            let location = m.ReverseIndex(index)
            let exitOpts =
                [for d in [Point2D.Left;Point2D.Right;Point2D.Up;Point2D.Down;
                        Point2D.UpLeft;Point2D.UpRight;Point2D.DownLeft;Point2D.DownRight] do
                    yield (d, (validExit location d))]

            let getDistance d =
                match (d.x <> 0, d.y <> 0) with
                    | (true, true) -> 1.45f
                    | _ -> 1.0f
            exitOpts
            |> List.filter (fun (_, exit) -> exit.IsSome)
            |> List.map (fun (d, exit) -> (exit.Value, getDistance d))
            |> List.filter (fun (exit, _) -> blocks |> List.contains exit |> not)

module MapGeneration =
    open Tiles
    open Rooms
    open Mapping

    type RLMap with
        /// Sets the tiles on the map based on a given sequence of X,Y coords.
        member m.CarveTiles(xys : seq<int * int>) =
            for x,y in xys do
                m.SetTile x y FloorTile
        /// Sets the tiles on the map based on a given list of Point2Ds.
        member m.CarveTiles(pts : list<Point2D>) =
            for p in pts do
                m.SetTile p.x p.y FloorTile

        /// Generates a basic map out of rectangular rooms and corridors.
        member m.GenerateBasicRooms(rng: RandomNumberGenerator, numRooms : int, minSize : int, maxSize : int) =
            let makeRoom() =
                let w, h = (rng.Next(minSize, maxSize + 1), rng.Next(minSize, maxSize + 1)) // Generate width and height between defined maximums.
                let x, y = (rng.Next(1, m.Width - w), rng.Next(1, m.Height - h))            // X and Y maximums are the map dimensions minus the width/height.
                RectangularRoom(x, y, w, h)

            let rooms = new List<RectangularRoom>()

            // Populate the list of rooms, retrying a room generation if it intersects with another.
            for _ in 1 .. numRooms do
                let mutable roomOK = false
                while not roomOK do
                    let newRoom = makeRoom()
                    roomOK <- true
                    for room in rooms do
                        if newRoom.Intersects(room) then roomOK <- false
                    if roomOK then rooms.Add(newRoom)

            // Carve out each room that we've constructed.
            for room in rooms do
                m.CarveTiles room.Inner

            // Carve tunnels between the rooms.
            for i in 1 .. numRooms - 1 do
                let center1, center2 = rooms[i - 1].Center, rooms[i].Center
                let direction = rng.Next(1, 3)
                let median =
                    match direction with
                        | 1 -> { x = center1.x; y = center2.y }
                        | _ -> { x = center2.x; y = center1.y }
                m.CarveTiles(pointLineOrtho(center1, median))
                m.CarveTiles(pointLineOrtho(center2, median))

            rooms |> Seq.toList |> List.map (fun r -> Room.Rectangular(r))

module LevelGeneration =
    open Rooms

    [<Struct>]
    type RoomPlacement =
        | Center
        | Random
        | Specified of p : Point2D

    /// Gets the point at which an entity should be placed based on the room type and the placement criteria.
    let GetPlacement(rng : RandomNumberGenerator, room : Room, place : RoomPlacement) =
        match room with
            | Rectangular(r) ->
                match place with
                    | Specified(p) -> p
                    | Center -> r.Center
                    | Random -> { x = rng.Next(r.x1 + 1, r.x2 - 1); y = rng.Next(r.y1 + 1, r.y2 - 1) }

    /// Updates an entity's position to a point within a room, with a criteria enum to specify its location.
    let PlaceInRoom(world : Container, rng : RandomNumberGenerator, eid : Eid, room : Room, place : RoomPlacement) =
        world.Get(eid).Add(GetPlacement(rng, room, place))
